iT邦幫忙

2021 iThome 鐵人賽

DAY 12
1
Modern Web

NestJS 帶你飛!系列 第 12 篇

[NestJS 帶你飛!] DAY12 - Interceptor

  • 分享至 

  • xImage
  •  

本系列文已出版成書「NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式」,感謝 iT 邦幫忙與博碩文化的協助。如果對 NestJS 有興趣、覺得這個系列文對你有幫助的話,歡迎前往購書,你的支持是我最大的寫作動力!

什麼是 Interceptor?

中文名稱為攔截器,受到 剖面導向程式設計 (Aspect Oriented Programming) 的啟發,為原功能的擴展邏輯,其特點如下:

  • 可以在執行 Controller 的方法 之前 與 之後 設計邏輯。
  • 執行於 Controller 的方法 之前 的 Interceptor 邏輯會在 Pipe 執行 之前 觸發。
  • 執行於 Middleware 之後。
  • 可以更動資料與 Exception。

https://ithelp.ithome.com.tw/upload/images/20210405/201193386hVCd5hrJj.png

設計 Interceptor

Interceptor 可以透過 CLI 產生:

$ nest generate interceptor <INTERCEPTOR_NAME>

注意:<INTERCEPTOR_NAME> 可以含有路徑,如:interceptors/hello-world,這樣就會在 src 資料夾下建立該路徑並含有 Interceptor。

這邊我建立一個 HelloWorldInterceptor 在 interceptors 資料夾下:

$ nest generate interceptor interceptors/hello-world

在 src 底下會看見一個名為 interceptors 的資料夾,裡面有 hello-world.interceptor.ts 以及 hello-world.interceptor.spec.ts:
https://ithelp.ithome.com.tw/upload/images/20210405/20119338z3UPmMzUqs.png

建立出來的 Interceptor 骨架如下,會發現 Interceptor 其實也是帶有 @Injectable 裝飾器的 class,不過它必須實作 NestInterceptor 介面,並設計 intercept(context: ExecutionContext, next: CallHandler) 方法:

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class HelloWorldInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle();
  }
}

CallHandler

CallHandler 為 Interceptor 的重要成員,它實作了 handle() 來調用路由處理的方法,進而導入對應的 Controller 方法,也就是說,如果在 Interceptor 不回傳 CallHandler 的 handle(),將會使路由處理失去運作。

由於 CallHandler 為 intercept 方法的參數,故其一定是在 intercept 中被呼叫,也就是說,可以在回傳 handle() 之前 寫一段邏輯,使其可以在進入 Controller 的方法前被執行,又因為 handle() 回傳的是 Observable,故可以透過 pipe 的方式 對回傳值做調整,使其可以在 Controller 的方法執行之後處理其他邏輯。

注意:handle() 是 Observable,我們把它作為 intercept 的回傳值是希望 Nest 可以去 subscribe 它,根據 Observable 的特性,若沒有去 subscribe 它則不會執行其內部邏輯,這也是為什麼不回傳 handle() 的話將會使路由處理失去運作的原因。

ExecutionContext

ExecutionContext 是繼承 ArgumentsHost 的 class,其提供了更多關於此請求的相關訊息,下方為它提供的兩個方法,透過這兩個方法可以大幅提升應用的靈活性:

取得 Controller Class

透過 getClass() 取得當前請求對應的 Controller Class:

const Controller: TodoController = context.getClass<TodoController>();

取得 Controller method

透過 getHandler() 取得當前請求對應的 Controller method,假設當前請求會呼叫 TodoController 的 getAll(),那就會回傳 getAll 這個函式:

const method: Function = context.getHandler();

使用 Interceptor

在使用之前,先將 hello-world.interceptor.ts 修改一下,在進入 Interceptor 時印出 Hello World! 並使用變數儲存進入的時間,再透過 tap 印出結束的時間與進入的時間差:

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class HelloWorldInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Hello World!');
    const input = Date.now();
    const handler = next.handle();
    return handler.pipe(
      tap(() => console.log(`${ Date.now() - input } ms`))
    );
  }
}

修改完以後就來使用此 Interceptor,透過 @UseInterceptors 裝飾器即可輕鬆套用,使用的方式大致上可以分成兩種:

  1. 單一資源:在 Controller 的方法中套用 @UseInterceptors 裝飾器,只會針對該資源套用。
  2. Controller:直接在 Controller 上套用 @UseInterceptors 裝飾器,會針對整個 Controller 中的資源套用。

下方以套用在 Controller 為例,修改 app.controller.ts:

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { HelloWorldInterceptor } from './interceptors/hello-world.interceptor';

@Controller()
@UseInterceptors(HelloWorldInterceptor)
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

透過瀏覽器查看 http://localhost:3000 會發現終端機出現了下方結果:

Hello World!
3 ms

全域 Interceptor

如果設計了一個共用的 Interceptor 要套用在所有資源上的話,只需要修改 main.ts 即可,透過 useGlobalInterceptors 來配置全域 Interceptor:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HelloWorldInterceptor } from './interceptors/hello-world.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new HelloWorldInterceptor());
  await app.listen(3000);
}
bootstrap();

用依賴注入實作全域 Interceptor

上面的方法是透過模組外部完成全域配置的,與 Pipe 一樣可以用依賴注入的方式,透過指定 Provider 的 token 為 APP_INTERCEPTOR 來實現,這裡是用 useClass 來指定要建立實例的類別:

import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HelloWorldInterceptor } from './interceptors/hello-world.interceptor';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_INTERCEPTOR,
      useClass: HelloWorldInterceptor
    }
  ]
})
export class AppModule {}

小結

Interceptor 可以在不修改 Controller 的情況下去擴充邏輯,是十分方便的功能。這裡附上今天的懶人包:

  1. Interceptor 執行於 Middleware 之後,但可執行於 Pipe 與 Controller 之前與之後。
  2. 可以在不變動原本邏輯的情況下去擴充邏輯。
  3. CallHandler 為重要成員,需要呼叫其 handle() 來讓路由機制得以運行。
  4. ExecutionContext 提供了 getClass() 與 getHandler() 來提升靈活性。
  5. 全域 Interceptor 可以透過依賴注入的方式實作。

上一篇
[NestJS 帶你飛!] DAY11 - Middleware
下一篇
[NestJS 帶你飛!] DAY13 - Guard
系列文
NestJS 帶你飛! 共 32 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
mihuartuanr
iT邦新手 3 級 ‧ 2021-11-05 11:40:12

CallHandler 為 Interceptor 的重要成員,它實作了 handle() 來調用路由處理的方法,進而導入對應的 Controller 方法,也就是說,如果在 Interceptor 不調用 CallHandler 的 handle(),將會使路由處理失去運作。

您好,根据文中示例,尝试了一下intercept返回不是CallHandler.handle()及其rxjs/operators处理结果的情况,发现也會使路由處理失去運作。

如果在 Interceptor 不調用 CallHandler 的 handle(),將會使路由處理失去運作。

改为"如果在 Interceptor 不調用 CallHandler 的 handle()并将其结果返回,將會使路由處理失去運作。"是不是更精确些呢

HAO iT邦研究生 1 級 ‧ 2021-11-05 15:47:48 檢舉

你好,不好意思,我說明的不夠精確,handle() 基本上就是呼叫 Controller 對應的 handler,並以 Observable 的形式讓我們可以用 RxJS 的技巧來針對 handler 處理完的資料做進一步的處理,如果返回值不是 handle() 所串聯的事件流,那 NestJS 就無法 subscribe 到它,而 Observable 沒有被 subscribe 的情況下是不會被執行的,就會造成所謂「路由處理失去運作」的情況。

我有更新文章進行補充了,感謝你的建議!

0

看到中間突然冒出個RxJS的東西卡住了好久不懂,去官往翻了一下還是不懂observable的概念...
https://rxjs-dev.firebaseapp.com/guide/observable

HAO iT邦研究生 1 級 ‧ 2022-11-22 20:40:10 檢舉

你好,Observable 是一種可觀察的物件,是 RxJS 裡面的核心角色之一,透過訂閱的方式來觀察這個物件提供的資料。
詳細的內容可以參考 Mike 大大的文章,基本上 Observable 是觀察者模式(Observable Pattern)的實作,當然,在 RxJS 的世界裡,有很多種可觀察的物件,在這篇文章中有非常詳細的說明。

不明 檢舉
【**此則訊息已被站方移除**】

我要留言

立即登入留言